#include <stdio.h>
#include <stdlib.h>
#include <string.h>
#include "pcb.h"
#include "sim.h"
#include "sched0.h"
#include "sched1.h"
#include "sched2.h"
#include "tracereader.h"
#include "io_queue.h"
#include "stats.h"
#include "common_sched.h"


/* func pointer to scheduler */
PCB* (*handleEvent)(Event, PCB*);
void (*initSched)();

/* global debug flag */
int debug;

/* clocks */
unsigned clock = 0; /* global */
unsigned last_clock;

/* stores info about the min event */
unsigned min_clock = 0;
Event min_event;

/* find next clock */
static void updateMinClock(Event e, unsigned proposed_clock);

PCB *tmp;

/* pretty print */
static void printUsage(char *argv[]);
static void printMinClock(Event e, unsigned val);

const char* eventNames[] = {"NULL", "Event:TERMPROC  ", "Event:TERMSLICE ", "Event:IOSTART   ", "Event:IOCOMPLETE", "Event:STARTPROC " };


int main ( int argc, char *argv[] )
{

	/* 
	 * Simulator Init 
	 * */
	int scheduler_number = 1;

	if( argc > 1){
		scheduler_number = atoi(argv[1]);
		printf("Scheduler:%d\n", scheduler_number);
	}

	if ( argc > 3 ) {
		debug = 1;
	    printf("Debug Enabled\n");
	} else {
		debug = 0;
	}
	
	/* point to selected scheduler */
	switch(scheduler_number){
		case 0:
			handleEvent = &handleEvent0;
			initSched = &initSched0;
			break;
		case 1:
			handleEvent = &handleEvent1;
		    initSched = &initSched1;
			break;
		case 2:
			handleEvent = &handleEvent2;
		    initSched = &initSched2;
			break;
		default:
			printUsage(argv);
	}

	/* open trace file */
	openTrace(argv[2]);

	/* init IO queue */
	initIOQueue();

	/* init scheduler */
	initSched();

	if(debug) printf("START CLOCK:%u\n", clock);

	/* 
	 * Start Simulator Main 
	 * */
	while(1)
	{
		min_clock = 0;

		/* context into kernel if proc running */
		if(NULL!=getPCB()){
			if(debug) printPCBState("CONTEXT:SAVE", getPCB());
			
			clock+=CONTEXTTIME;
			
			incrementIOQueue(); /* runs during context */
		

			/* handle terminating process */
			if(isProcessComplete(getPCB())){
				tmp = handleEvent(TERMPROC, getPCB());
				if(debug) printPCBState(eventNames[TERMPROC], getPCB());
				completed(getPCB());
				clearPCB();
				setPCB(tmp, TERMPROC);
			}
		}

		/* handle start IO */
		if( IOSTART == min_event )
		{
			if(debug) printPCBState(eventNames[IOSTART], getPCB());
			tmp = handleEvent(IOSTART, getPCB()); /* send to sched */
			doIO(getPCB()); /* send to IO */
			clearPCB(); /* clear current process in common sched */
			setPCB(tmp, IOSTART); /* pass specific proc to common sched */
		}
		
		while(!noCompletedProcs()){

			PCB* io_complete = getIOCompletedProc();

			/* sanity check */
            if(getCompletedIO(io_complete) > getTimeLineSize(io_complete))
			{
				fprintf(stderr, "Too many completed io ops.\n");
				exit(EXIT_FAILURE);
			}

			if(debug) printPCBState(eventNames[IOCOMPLETE], io_complete);
			tmp = handleEvent(IOCOMPLETE, io_complete);

			/* proc could end with IO */
			if(isProcessComplete(io_complete)) 
			{
				if(debug) printPCBState(eventNames[TERMPROC], io_complete);
				tmp = handleEvent(TERMPROC, io_complete);
				completed(io_complete);
				setPCB(tmp, TERMPROC);
			} else {
				setPCB(tmp, IOCOMPLETE);
			}
		}

		/* handle new process(es) */
		while( !isTraceComplete() && clock >= nextProcessStartTime() )
		{
			PCB* proc = getNextProcess();

			if(debug) printPCBState(eventNames[PROCESSSTART], proc);

			if( NULL!= proc )
			{
				proc = handleEvent(PROCESSSTART, proc); /* get proc to run from specific sched */
				setPCB(proc, PROCESSSTART);	/* pass to common sched */
			} else {
				fprintf(stderr, "Null program from trace, shouldn't be possible.\n");
				exit(EXIT_FAILURE);
			}
		}

		/* interface between common sched and specific sched */
		if(TERMSLICE == min_event) {
			/* proc timeslice is done */
			tmp = handleEvent(TERMSLICE, getPCB());
			if(debug) printPCBState(eventNames[TERMSLICE], getPCB());

			/* could be complete after slice */
			if(isProcessComplete(getPCB())){
				tmp = handleEvent(TERMPROC, getPCB());
				if(debug) printPCBState(eventNames[TERMPROC], getPCB());
				completed(getPCB());
				clearPCB();
				setPCB(tmp, TERMPROC);
			} else {
				setPCB(tmp, TERMSLICE);
			}
		}

		/* 
	 	* Find min clock jump, record next event
	 	* 
	 	* */
		min_event = 0;


		/* next process start time */
		if( !isTraceComplete() )
		{
			/* - clock to make relative */
			updateMinClock(PROCESSSTART, nextProcessStartTime() - clock);
		}

		/* Next IO complete */
        if( getIOLength() ){
        	/* make relative */
			updateMinClock(IOCOMPLETE, nextIODoneTime() - clock);
        }

		/* TERMPROC/TERMSLICE/IOSTART from common sched */
		if( NULL != getPCB() ){
			updateMinClock(nextProcessEvent(), nextProcessEventTime());
		}

        /* context load if there is one to run */
        if(NULL!=getPCB())
        {
			if(debug) printPCBState("CONTEXT:LOAD", getPCB());
            clock += CONTEXTTIME;
            incrementIOQueue(); /* still runs on context time */
        }

		/* increment the clock, decide the delta */
		last_clock = clock;
		clock += min_clock;

		if( 0 == min_clock && NULL == getPCB() ){
			setLastClock(last_clock); /* for stats */
		    printf("Simulation completed.\n");
		    printStats();
		    exit(EXIT_SUCCESS);
		}
	
		/* print debug or some info to note progress */
		if(debug) {
			printf("END CLOCK:%u\n", last_clock);
			printf("START CLOCK:%u\n", clock);
		}
		
		/* update times in modules */
		incrementRunningProcess(clock - last_clock);
		incrementIOQueue();

	}

	return 0;
}

static void printUsage(char* argv[])
{
	fprintf(stderr, "usage: %s <1 or 2> <trace file> [debug]\n\nPlease check usage\n", argv[0]);
	exit(EXIT_FAILURE);

}

static void updateMinClock(Event e, unsigned proposed_clock){
	if( min_clock >= proposed_clock || 0 == min_clock ){

		  min_clock = proposed_clock;
          min_event = e;
		  if(debug) printMinClock(e, proposed_clock);
	} 
}

static void printMinClock(Event e, unsigned val)
{

	if( 0 == val ) return;

	switch(e){
		case IOSTART:
		case TERMSLICE:
		case TERMPROC:
			printf("\tmin_clock(%s):%u PID:%u CLK:%u\n", 
				eventNames[e], val,
				getPid(getPCB()),
				clock
				);
			break;
		default:	
			printf("\tmin_clock(%s):%u CLK:%u\n", eventNames[e], val, clock);
			break;
	}
}
